本次源码分析是以 GitHub 上 Retrofit 仓库 3.11版本来进行的分析,同时以官方网站文档作为参考。如后续版本有较大改变,恕不另行修改。

自定义哪些内容?

上一篇中,我们分析了 OkHttp 在进行网络请求时候的,一个大致原理。发现其比较简单的,不像 Retrofit 比较复杂,简单来说就是 生成请求请求调用返回响应。因此自定义内容有限,主要还是以拦截器 interceptor 为主,本篇我们就以 HttpLoggingInterceptor 来分析下,如果要自定义拦截器需要注意哪些方面。

ApplicationInterceptor NetworkInterceptor

在分析 HttpLoggingInterceptor 之前,我们先来看看,这两个有啥区别。 上一篇中,我们在分析 getResponseWithInterceptorChain() 其实有说过,二者的差别在于 调用先后顺序 的不同。ApplicationInterceptor 在所有自带的拦截器调用之前调用,而 NetworkInterceptor 在 ConnectInterceptor 调用后(socket 连接建立之后),CallServerInterceptor 调用之前(请求数据之前)调用。这是二者唯一的差别。

我们给出一个例子来进行说明:

  OkHttpClient client = new OkHttpClient.Builder()
      .addInterceptor(new Interceptor() {
        @Override public Response intercept(Chain chain) throws IOException {
          System.out.println("Request Chain url: "+chain.request().url());
          Response response = chain.proceed(chain.request());
          System.out.println("Response Chain url: "+response.request().url()+" isRedirect: "+response.isRedirect());
          return response;
        }
      })
      .build();

  String run(String url) throws IOException {
    Request request = new Request.Builder()
        .url(url)
        .build();
    try (Response response = client.newCall(request).execute()) {
      return String.valueOf(response.request().url());
    }
  }

  public static void main(String[] args) throws IOException {
    GetExample example = new GetExample();
    String response = example.run("http://google.com");
    System.out.println("final url: "+response);
  }

代码很简单就是生成一个客户端,然后对给出的 url 调用 GET 方法,将收到的 Response 中 Request 部分的 Url 打印出来。Interceptor 部分也是很简单,对于请求没做任何处理,只是打印出来其 Url 并打印出响应体中,是否是重定向的。可以得到下面的结果:

Request Chain url: http://google.com/
Response Chain url: http://www.google.com/ isRedirect: false
final url: http://www.google.com/

我们在请求的时候传入的是一个 http 网址,网站解析后将其自动重定向到 https 的 url,因此最后的 GET 方法是使用的 https 的 url 来进行请求,同时最终返回的响应也是重定向后的网址。

假如我们换成 addNetworkInterceptor 再进行请求,会得到如下的结果

Request Chain url: http://google.com/
Response Chain url: http://google.com/ isRedirect: true
Request Chain url: http://www.google.com/
Response Chain url: http://www.google.com/ isRedirect: false
final url: http://www.google.com/

我们可以看到,如果是使用了 NetworkInterceptor 则会将每一次 socket 连接和服务端通信的情况展现出来,而不是仅仅展现最后一次返回值。

大致上,我们也明白二者是怎么样的区别了。因此可以简单的给两类拦截器下个结论:

ApplicationInterceptor 在一次请求的过程中只会调用一次; NeteorkInterceptor 可能调用不止一次。
ApplicationInterceptor 请求的时候不需要考虑是否重定向,根据其 reponse 读出 isRedirect 一定是 false。
当有缓存的时候,ApplicationInterceptor 一样会调用,但是 NetworkInteceptor 则不会被调用。

自定义拦截器

头部拦截器

有了上面的经验,我们可以开始自己来编写拦截器了。现在考虑一种需求:Api 请求的时候大多数的时候都要带上 Token 来进行验证,如果每次请求的话都将其以参数的方式来构造不仅麻烦还容易出错,但配上拦截器的话就会简单很多。

private static class HeadInterceptor implements Interceptor{
  @Override public Response intercept(Chain chain) throws IOException {
    Request.Builder newReq = chain.request().newBuilder();
    newReq.header("Authorization","Bearer "+TokenWrapper.INSTANCE.getToken());
    return chain.proceed(newReq.build());
  }
}

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HeadInterceptor())
    .addInterceptor(interceptor)
    .build();

example.run("http://httpbin.org/get").body().print()

log:
--> GET http://httpbin.org/get
Authorization: Bearer XXXXXXXXXXXX
--> END GET
<-- 200 OK http://httpbin.org/get (776ms)
...
<-- END HTTP
final url: {
  "args": {},
  "headers": {
    "Accept-Encoding": "gzip",
    "Authorization": "Bearer XXXXXXXXXXX",
    "Connection": "close",
    "Host": "httpbin.org",
    "User-Agent": "okhttp/3.12.0-SNAPSHOT"
  },
  "url": "http://httpbin.org/get"
}

我们把一个头部拦截器实例传入到 OkHttp ,在每次请求的时候,都将 Token 写入到 header 里面。我们根据 logger 拦截器看到,自定义传入的 header 只有 Authorization 。返回的 ResponseBody 也可以看到服务器也确实是收到了我们的 Token

参数拦截器

在讲参数拦截器之前,我们先来看看FormBody MultipartBody两个,库自带 RequestBody 的实现。 上一篇中,我们看了 RequestBody 的源码,其中包括 MediaType 和 content,这两个部分。 前者表面数据类型,后者表明数据内容。而使用 FormBody 可以便利的生成表单的 RequestBody,就像使用map 一样,调用 add 方法来传入需要的键值对。相反,如果直接使用 RequestBody.create() 反而不是很方便。

RequestBody formBody = new FormBody.Builder().add("user", "Arirus")
    .add("passwd","qqqqqq").build();
RequestBody requestBody =
    RequestBody.create(MediaType.get("application/x-www-form-urlencoded"), "user=Arirus&passwd=qqqqqq");
Request request =
    new Request.Builder().url("http://httpbin.org/post").post(requestBody).build();

log:
{
  ...
  "form": {
    "passwd": "qqqqqq",
    "user": "Arirus"
  }
  ...
}

二者都可以正常提交表单,但是明显第一种方式更简单一些。当然除了这个之外还有另一个重要的原因:

RequestBody.create() 传入 content 默认都是经过 url-encode 的

这个可能不好理解,我举个例子:假如 passwd 传入的值为 qqq+qqq%6D,分别运行下两种请求,结果如下。

// requestBody 响应
"form": {
  "passwd": "qqq qqqm",
  "user": "Arirus"
},
// formBody 响应
"form": {
  "passwd": "qqq+qqq%6D",
  "user": "Arirus"
},

我们发现使用 requestBody 提交表单,数据好像是被转义了一样,而 formBody 提交表单没有这个问题。 这就是上面原因导致的结果:+ 会被转义为一个空格,%HH 会被转义成一个字符。所以推荐使用 requestBody 来提交表单数据。同样对于MultipartBody,如果手动传入数据会比较复杂,建议使用其进行数据传递。

说完上面这些,我们来分析下如何对于传递参数动手脚

@Override public Response intercept(Chain chain) throws IOException {
  System.out.println(chain.request().body());
  if (!(chain.request().body() instanceof FormBody)) return chain.proceed(chain.request());

  for (int i = 0; i < ((FormBody) chain.request().body()).size(); i++) {
    System.out.printf("Key:%s Value:%s \n",
        ((FormBody) Objects.requireNonNull(chain.request().body())).name(i),
        ((FormBody) Objects.requireNonNull(chain.request().body())).value(i));
  }

  return chain.proceed(chain.request());
}
log:
Key:user Value:Arirus
Key:passwd Value:qqq+qqq%6D

这样我们发现如果 RequestBody 是 FormBody 的实例,就可以遍历键值对的 list,获取所有参数。对于 MultipartBody 则是相同原理,其内部有 Part 的 List,可以遍历得到每个 Part。

小结

本篇当中主要是分析了,两类拦截器的区别,同时分析使用 FormBody 和 MultiPartBody 来生成 RequestBody 的优点,最后分别给出一个头部拦截器和参数拦截器的例子。相对于 Retrofit,OkHttp 更简单一些,那么这个系列文章就到这里结束了,有缘再见。

results matching ""

    No results matching ""